package typeexpr

import (
    "bytes"
    "fmt"
    "sort"

    "Havoc/pkg/profile/yaotl/hclsyntax"

    "Havoc/pkg/profile/yaotl"
    "github.com/zclconf/go-cty/cty"
)

// Type attempts to process the given expression as a type expression and, if
// successful, returns the resulting type. If unsuccessful, error diagnostics
// are returned.
func Type(expr hcl.Expression) (cty.Type, hcl.Diagnostics) {
    return getType(expr, false)
}

// TypeConstraint attempts to parse the given expression as a type constraint
// and, if successful, returns the resulting type. If unsuccessful, error
// diagnostics are returned.
//
// A type constraint has the same structure as a type, but it additionally
// allows the keyword "any" to represent cty.DynamicPseudoType, which is often
// used as a wildcard in type checking and type conversion operations.
func TypeConstraint(expr hcl.Expression) (cty.Type, hcl.Diagnostics) {
    return getType(expr, true)
}

// TypeString returns a string rendering of the given type as it would be
// expected to appear in the HCL native syntax.
//
// This is primarily intended for showing types to the user in an application
// that uses typexpr, where the user can be assumed to be familiar with the
// type expression syntax. In applications that do not use typeexpr these
// results may be confusing to the user and so type.FriendlyName may be
// preferable, even though it's less precise.
//
// TypeString produces reasonable results only for types like what would be
// produced by the Type and TypeConstraint functions. In particular, it cannot
// support capsule types.
func TypeString(ty cty.Type) string {
    // Easy cases first
    switch ty {
    case cty.String:
        return "string"
    case cty.Bool:
        return "bool"
    case cty.Number:
        return "number"
    case cty.DynamicPseudoType:
        return "any"
    }

    if ty.IsCapsuleType() {
        panic("TypeString does not support capsule types")
    }

    if ty.IsCollectionType() {
        ety := ty.ElementType()
        etyString := TypeString(ety)
        switch {
        case ty.IsListType():
            return fmt.Sprintf("list(%s)", etyString)
        case ty.IsSetType():
            return fmt.Sprintf("set(%s)", etyString)
        case ty.IsMapType():
            return fmt.Sprintf("map(%s)", etyString)
        default:
            // Should never happen because the above is exhaustive
            panic("unsupported collection type")
        }
    }

    if ty.IsObjectType() {
        var buf bytes.Buffer
        buf.WriteString("object({")
        atys := ty.AttributeTypes()
        names := make([]string, 0, len(atys))
        for name := range atys {
            names = append(names, name)
        }
        sort.Strings(names)
        first := true
        for _, name := range names {
            aty := atys[name]
            if !first {
                buf.WriteByte(',')
            }
            if !hclsyntax.ValidIdentifier(name) {
                // Should never happen for any type produced by this package,
                // but we'll do something reasonable here just so we don't
                // produce garbage if someone gives us a hand-assembled object
                // type that has weird attribute names.
                // Using Go-style quoting here isn't perfect, since it doesn't
                // exactly match HCL syntax, but it's fine for an edge-case.
                buf.WriteString(fmt.Sprintf("%q", name))
            } else {
                buf.WriteString(name)
            }
            buf.WriteByte('=')
            buf.WriteString(TypeString(aty))
            first = false
        }
        buf.WriteString("})")
        return buf.String()
    }

    if ty.IsTupleType() {
        var buf bytes.Buffer
        buf.WriteString("tuple([")
        etys := ty.TupleElementTypes()
        first := true
        for _, ety := range etys {
            if !first {
                buf.WriteByte(',')
            }
            buf.WriteString(TypeString(ety))
            first = false
        }
        buf.WriteString("])")
        return buf.String()
    }

    // Should never happen because we covered all cases above.
    panic(fmt.Errorf("unsupported type %#v", ty))
}
